<?php

/**
 * Description of CSVDB
 *
 * @author nk11k
 */
class CSVDB {

    /** #@+
     * @var String
     */
    
    private $HOSTNAME;
    private $USERNAME;
    private $PASSWORD;
    private $DBNAME;

    /**
     * Имя файла таблицы сообщений в базе данных
     */
    private $TBLMESSAGESFILE;
    /**
     * Имя файла таблицы комментариев в базе данных
     */
    private $TBLCOMMENTSFILE;

    /** #@- */

    /**
     *
     * @var array двумерный массив данных из CSV-файла: комментарии
     */
    private $csvarraycmts;

    /**
     *
     * @var array двумерный массив данных из CSV-файла: сообщения
     */
    private $csvarraymsgs;

    /**
     * Константа для вызова отладочного кода
     */
    const DBG = TRUE;

    /**
     * Дескриптор соединения с базой данных (ресурс)
     *
     * @access private
     * $var resource
     */
    private $link;

    /**
     * Конструктор класса
     *
     * Получает дескрипторы и парсит csv-файлы 
     */
    function __construct() {
        if (self::DBG) echo '<br> using CSVDB';
        global $cfg;
        $this->csvarraycmts = array();
        $this->csvarraymsgs = array();
        $this->TBLMESSAGESFILE = $cfg->msgtable . '.csv';
        $this->TBLCOMMENTSFILE = $cfg->cmttable . '.csv';
        $this->csvarraycmts = $this->parseCSVIntoArray($this->TBLCOMMENTSFILE);
        $this->csvarraymsgs = $this->parseCSVIntoArray($this->TBLMESSAGESFILE);
    }

    /**
     * Метод для подключения к базе MySQL через PDO
     *
     * Устанавливает соединение с базой данных MySQL, выбирает базу и устанавливает кодировку UTF8
     */
    public function connectToMySQLDB() {
        echo '<br> connect through PDO';
        try {
            $this->link = new PDO(
                    "mysql:host=". $this->HOSTNAME . ";dbname=" . $this->DBNAME,
                    $this->USERNAME,
                    $this->PASSWORD,
                    array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));
            if (self::DBG) echo '<br>Connected to database';
            /* добавляем поддержку исключений */
            $this->link->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            $this->link->setAttribute(PDO::ATTR_EMULATE_PREPARES, TRUE); //bug: https://bugs.php.net/bug.php?id=44639&thanks=6
        } catch (PDOException $e) {
            echo $e->getMessage();
        }
    }

    function __destruct() {
        $this->link = NULL;
    }

    /**
     * Метод для получения всех сообщений из таблицы
     *
     * @access public
     * @return array двумерный массив строк, полученных в результате чтения файла,
     *  или FALSE, если файл пуст/не имеет нужных прав
     */
    public function getAllMessages() {
        try {
            return array_reverse($this->csvarraymsgs);
        } catch (Exception $e) {
            echo "<br>Error: " . $e->getMessage();
            return FALSE;
        }
    }

    /**
     * Метод для получения сообщения из таблицы по его ID
     *
     * @access public
     * @param String $messageid номер сообщения ленты, которое следует запросить из базы
     * @return array строки, полученные в результате чтения файла, или FALSE, если запрос пуст/не удался
     */
    public function getMessageByID($messageid) {
        if (!is_numeric($messageid)) {
            if (self::DBG) {
                echo ('<br>getMessageByID: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
            }
            return FALSE;
        }
        try {
            foreach ($this->csvarraymsgs as $index => $csvrow) {
                if ($csvrow['messageid'] === $messageid)
                    return $this->csvarraymsgs[$index];
            }
        } catch (Exception $e) {
            echo "<br>Error:" . $e->getMessage();
            return FALSE;                   
        }
        return FALSE;
    }

    /**
     * Метод для получения всех комментариев из таблицы
     *
     * @access public
     * @param integer $messageid номер сообщения ленты, для которого следует получить комментарии
     * @return array двумерный массив строк, полученных в результате чтения файла, или FALSE, если запрос пуст/не удался
     */
    public function getAllCommentsToMessage($messageid) {
        if (!is_numeric($messageid)) {
            if (self::DBG) {
                echo ('<br>getAllCommentsToMessage: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
            }
            return FALSE;
        }
        try { /* таблицы должны быть отсортированы по столбцам messageid (ASC) и
               * commenttid (ASC) */
            $rowsN = count($this->csvarraycmts);
            $commentstomsg = array();
            for ($i = 0; $i < $rowsN; $i++) {
                if ($this->csvarraycmts[$i]['messageid'] === $messageid) {
                    /* нашли первый комментарий, они идут по порядку */
                    $commentstomsg[] = $this->csvarraycmts[$i++];
                    for ($j = $i; $j < $rowsN; $j++) {
                        if ($this->csvarraycmts[$j]['messageid'] === $messageid) {
                            $commentstomsg[] = $this->csvarraycmts[$j];
                            continue;
                        }
                        break; /* если дальше комментариев нет, то до конца смотреть не надо */
                    }
                    break; /* комментариев больше нет */
                }
            }
            return $commentstomsg;
        } catch (Exception $e) {
            echo "<br>Error:" . $e->getMessage();
            return FALSE;
        }
        return FALSE;
    }

    /**
     * Метод для получения комментария из таблицы по его ID
     *
     * @access public
     * @param integer $messageid номер сообщения ленты, для которого следует получить комментарий
     * @param integer $cmtid номер комментария
     * @return array строки, полученные в результате запроса, или FALSE, если запрос пуст/не удался
     */
    public function getCommentaryByID($messageid, $cmtid) {
        if (!is_numeric($messageid) || !is_numeric($cmtid)) {
            if (self::DBG) {
                echo ('<br>getCommentaryByID: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$cmtid is ' . gettype($cmtid));
            }
            return FALSE;
        }
        try {
            foreach ($this->csvarraycmts as $index => $csvrow) {
                if ($csvrow['messageid'] === $messageid && $csvrow['commentid'] === $cmtid)
                    return $this->csvarraycmts[$index];
            }
        } catch (Exception $e) {
            echo "<br>Error:" . $e->getMessage();
            return FALSE;                   
        }
        return FALSE;
    }
    /**
     * Метод для добавления сообщения в ленту
     *
     * @access public
     * @param string $text текст сообщения
     * @param string $author имя автора сообщения
     * @return boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    public function addMessage($text, $author) {
        if (!is_string($text) || !is_string($author)) {
             if (self::DBG) {
                echo ('<br>addMessage: wrong argument type');
                echo ('<br>$text is ' . gettype($text));
                echo ('<br>$author is ' . gettype($author));
             }
            return FALSE;
        }
        /* Автоинкремент индексов */
        try {
            $msgid = $this->nextTableRowIndex($this->csvarraymsgs, 'messageid');
            $now = date("Y-m-d H:i:s");
            /* Добавляем в базу, сортируем и записываем в файл */
            $this->csvarraymsgs[] = array(
                        'messageid' => $msgid,
                        'text' => $text,
                        'author' => $author,
                        'datetime' => $now,
                        'modified' => $now);
            $this->sortMessagesTable();
            $this->writeArrayToCSV($this->csvarraymsgs, $this->TBLMESSAGESFILE);
            return TRUE;
        } catch (Exception $e) {
            echo "<br>Error:" . $e->getMessage();
            return FALSE;            
        }
    }

    /**
     * Метод для добавления комментария в ленту
     *
     * @access public
     * @param integer $messageid номер сообщения ленты, для которого следует добавить комментарий
     * @param string $text текст сообщения
     * @param string $author имя автора сообщения
     * @return boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    public function addCommentary($messageid, $text, $author) {
        if (!is_numeric($messageid) || 
            !is_string($text)       || 
            !is_string($author)) {
             if (self::DBG) {
                echo ('<br>addCommentary: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$text is ' . gettype($text));
                echo ('<br>$author is ' . gettype($author));
             }
            return FALSE;            
        }
        
        /* Автоинкремент индексов */
        try {
            $cmtid = $this->nextTableRowIndex($this->csvarraycmts, 'commentid');
            $now = date("Y-m-d H:i:s");
            /* Добавляем в базу, сортируем и записываем в файл */
            $this->csvarraycmts[] = array(
                        'messageid' => $messageid,
                        'commentid' => $cmtid,
                        'text' => $text,
                        'author' => $author,
                        'datetime' => $now);
            $this->sortCommentsTable();
            $this->writeArrayToCSV($this->csvarraycmts, $this->TBLCOMMENTSFILE);
            return TRUE;
        } catch (Exception $e) {
            echo "<br>Error:" . $e->getMessage();
            return FALSE;            
        }
    }

    /**
     * Метод для обновления сообщения в ленте
     *
     * @access public
     * @param integer $messageid номер сообщения ленты, который следует изменить
     * @param string $text текст сообщения
     * @param string $author имя автора сообщения
     * @return boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    public function updateMessage($messageid, $text, $author) {
        if (!is_numeric($messageid) || 
            !is_string($text)       || 
            !is_string($author)) {
             if (self::DBG) {
                echo ('<br>updateMessage: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$text is ' . gettype($text));
                echo ('<br>$author is ' . gettype($author));
             }
            return FALSE;
            }            
        try {
            /* изменяем в массиве сразу текст, автора, дату модификации */
            $now = date("Y-m-d H:i:s");
            foreach ($this->csvarraymsgs as $index => $csvrow) {
                if ($csvrow['messageid'] === $messageid)
                    $this->csvarraymsgs[$index] = array(
                        'messageid' => $messageid,
                        'text' => $text,
                        'author' => $author,
                        'datetime' => $this->csvarraymsgs[$index]['datetime'],
                        'modified' => $now);
            }
            
            /* Записываем в файл */
            $this->writeArrayToCSV($this->csvarraymsgs, $this->TBLMESSAGESFILE);
            return TRUE;
        } catch (Exception $e) {
            echo "<br>Error:" . $e->getMessage();
            return FALSE;            
        }
    }

    /**
     * Метод для обновления комментария в ленте
     *
     * @access public
     * @param integer $messageid номер сообщения ленты, который содержит комментарий
     * @param integer $cmtid номер комментария, который следует изменить
     * @param string $text текст сообщения
     * @param string $author имя автора сообщения
     * @return boolean TRUE в случае успешного завершения метода, иначе - FALSE
     */
    public function updateCommentary($messageid, $cmtid, $text, $author) {
        if (!is_numeric($messageid) || 
            !is_numeric($cmtid)     || 
            !is_string($text)       || 
            !is_string($author)) {
             if (self::DBG) {
                echo ('<br>updateCommentary: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$cmtid is ' . gettype($cmtid));
                echo ('<br>$text is ' . gettype($text));
                echo ('<br>$author is ' . gettype($author));
            }
            return FALSE;
           }        
        try {
            // изменяем в массиве сразу текст, автора
            foreach ($this->csvarraycmts as $index => $csvrow) {
                if ($csvrow['messageid'] === $messageid && $csvrow['commentid'] === $cmtid)
                    $this->csvarraycmts[$index] = array(
                        'messageid' => $messageid,
                        'commentid' => $cmtid,
                        'text' => $text,
                        'author' => $author,
                        'datetime' => $this->csvarraycmts[$index]['datetime']);
            }
            
            /* Записываем в файл */
            $this->writeArrayToCSV($this->csvarraycmts, $this->TBLCOMMENTSFILE);
            return TRUE;
        } catch (Exception $e) {
            echo "<br>Error:" . $e->getMessage();
            return FALSE;
        }

    }

    /**
     * Метод для удаления сообщения из ленты
     * 
     * Удаляет сообщения по его $messageid, при этом комментарии к нему удаляются,
     *  поскольку они связаны через домен 'messageid'
     * 
     * @access public
     * @param integer $messageid номер сообщения
     * @return boolean в случае ошибки возвращает FALSE и наоборот
     */
    public function deleteMessage($messageid) {
        if (!is_numeric($messageid)) {
            if (self::DBG) {
                echo ('<br>deleteMessage: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
            }
            return FALSE;
        }
        try {
            $this->removeElementWithValue($this->csvarraymsgs, 'messageid', $messageid);
            $this->removeCommentsToMessage($messageid);
            $this->sortCommentsTable();
            $this->sortMessagesTable();
            $this->writeArrayToCSV($this->csvarraycmts, $this->TBLCOMMENTSFILE);
            $this->writeArrayToCSV($this->csvarraymsgs, $this->TBLMESSAGESFILE);
            return TRUE;
        } catch (Exception $e) {
            echo "<br>Error:" . $e->getMessage();
            return FALSE;            
        }
    }
    
    /**
     * Метод для удаления комментария из ленты
     * 
     * Удаляет элемент из массива, сортирует таблицу и записывает её в файл
     * 
     * @access public
     * @param integer $messageid номер сообщения
     * @param integer $commentaryid номер комментария
     * @return boolean в случае ошибки возвращает false и наоборот
     */
    public function deleteCommentary($messageid, $commentaryid) {
        if (!is_numeric($messageid) || !is_numeric($commentaryid)) {
            if (self::DBG) {
                echo ('<br>deleteCommentary: wrong argument type');
                echo ('<br>$messageid is ' . gettype($messageid));
                echo ('<br>$commentaryid is ' . gettype($commentaryid));
            }
            return FALSE;
        }
        try {
            $this->removeElementWithValue($this->csvarraycmts, 'commentid', $commentaryid);
            $this->sortCommentsTable();
            $this->writeArrayToCSV($this->csvarraycmts, $this->TBLCOMMENTSFILE);
            return TRUE;
        } catch (Exception $e) {
            echo "<br>Error:" . $e->getMessage();
            return FALSE;
        }
    }
    /**
     * Метод для обработки CSV-файла и сохранении его структуры в массив
     * 
     * Вызывает метод для обработки таблицы сообщений либо комментариев
     * 
     * @access private
     * @param string имя файла для обработки парсером
     * @return boolean TRUE в случае успеха, иначе - FALSE
     */
    private function parseCSVIntoArray($filename) {
        if ($filename == $this->TBLMESSAGESFILE) {
            return $this->parseCSVMessagesIntoArray($filename);
        }
        else if ($filename == $this->TBLCOMMENTSFILE) {
            return $this->parseCSVCommentsIntoArray($filename);
        }
        return FALSE;
    }
    /**
     * Метод для обработки CSV-файла таблицы сообщений и сохранении его структуры в массив
     * 
     * Выполняет обработку таблицы сообщений, получая построчно данные из CSV-файла
     * 
     * @access private
     * @param string имя файла для обработки парсером
     * @return boolean TRUE в случае успеха, иначе - FALSE
     */
    private function parseCSVMessagesIntoArray($filename) {
        if (($handle = fopen($filename, "r")) !== FALSE) {
            # Индекс строки двумерного массива = 0.
            $nn = 0;
            $parsed = array();
            while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
                # Количество столбцов в строке:
                $c = count($data);
                # Заполняем строку:
                $parsed[$nn]['messageid'] = $data[0];
                $parsed[$nn]['text'] = $data[1];
                $parsed[$nn]['author'] = $data[2];
                $parsed[$nn]['datetime'] = $data[3];
                $parsed[$nn]['modified'] = $data[4];
                $nn++;
            }
            fclose($handle);
            return $parsed;
        }
        return FALSE;
    }
    /**
     * Метод для обработки CSV-файла таблицы комментариев и сохранении его структуры в массив
     * 
     * Выполняет обработку таблицы сообщений, получая построчно данные из CSV-файла
     * 
     * @access private
     * @param string имя файла для обработки парсером
     * @return boolean TRUE в случае успеха, иначе - FALSE
     */
    private function parseCSVCommentsIntoArray($filename) {
        if (($handle = fopen($filename, "r")) !== FALSE) {
            # Индекс строки двумерного массива = 0.
            $nn = 0;
            $parsed = array();
            while (($data = fgetcsv($handle, 1000, ",")) !== FALSE) {
                # Количество столбцов в строке:
                $c = count($data);
                # Заполняем строку:
                $parsed[$nn]['messageid'] = $data[0];
                $parsed[$nn]['commentid'] = $data[1];
                $parsed[$nn]['text'] = $data[2];
                $parsed[$nn]['author'] = $data[3];
                $parsed[$nn]['datetime'] = $data[4];
                $nn++;
            }
            fclose($handle);
            return $parsed;
        }
        return FALSE;
    }
    /**
     * Метод для формирования и записи данных из массива в CSV-файл
     * 
     * Получает в качестве параметров массив и имя нового CSV-файла, построчно заполняет его
     * при помощи функции fputcsv
     * 
     * @access private
     * @param array $array 2D-массив, содержащий данные для вывода в CSV-файл
     * @param string $filename имя выходного файла
     */
    private function writeArrayToCSV($array, $filename) {
        if (($handle = fopen($filename, "w")) !== FALSE) {
            foreach ($array as $fields) 
            {
                fputcsv($handle, $fields);
            }
            fclose($handle);
        }
        
    }
    
    /**
     * Метод для получения следующего значения индекса, аналог функции AUTO_INCREMENT в MySQL
     * 
     * @access private
     * @param array $table 2D-массив таблицы
     * @param string $column название столбца, в котором находятся индексы
     * @return integer значение индекса
     */
    private function nextTableRowIndex($table, $column) {
        return max($this->array_column($table, $column)) + 1;
    }

    /**
     * Метод для среза двумерного массива по одному столбцу (добавлено в php 5.5)
     * 
     * @access private
     * @param array $arr двумерный массив, из которого производится выборка
     * @param string $column имя или номер столбца
     * @return array массив значений столбца $column
     */
    private function array_column(array $arr, $column) {
        $extracted = array();
        foreach ($arr as $item) {
            $extracted[] = $item[$column];
        }
        return $extracted;
    }

    /**
     * Метод для сортировки таблицы комментариев
     * 
     * Сначала происходит выбор значений столбцов messageid commentid, затем
     * происходит сортировка по столбцу messageid, затем по commentid:
     * для этого используется библиотечная функция array_multisort
     * 
     * @access private
     */
    private function sortCommentsTable() {
        $messagesids = $this->array_column($this->csvarraycmts, 'messageid');
        $commentsids = $this->array_column($this->csvarraycmts, 'commentid');
        array_multisort($messagesids, $commentsids, $this->csvarraycmts);
    }

    /**
     * Метод для сортировки таблицы сообщений
     * 
     * Используется библиотечная функция array_multisort
     * 
     * @access private
     */
    private function sortMessagesTable() {
        $messagesids = $this->array_column($this->csvarraymsgs, 'messageid');
        array_multisort($messagesids, $this->csvarraymsgs);
    }

    /**
     * Метод для удаления всех комментариев к сообщению $msgid
     * 
     * Враппер для вызова функции removeElementWithValue
     * 
     * @access private
     * @param integer $msgid номер сообщения, к которому удаляются все комментарии
     */
    private function removeCommentsToMessage($msgid) {
        $this->removeElementWithValue($this->csvarraycmts, 'messageid', $msgid);
    }
    /**
     * Метод для удаления элементов (строк) по заданной паре ключ (столбец) => значение
     * 
     * @access private
     * @param array $array 2D-массив
     * @param string $key имя столбца
     * @param integer $value значение столбца
     */
    private function removeElementWithValue(& $array, $key, $value){
        foreach($array as $subKey => $subArray){
            if($subArray[$key] == $value){
                unset($array[$subKey]);
            }
        }
    }
}
